package com.googlecode.cswish.util;

import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.views.freemarker.FreemarkerManager;
import org.apache.struts2.views.freemarker.ScopesHashModel;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;

import com.googlecode.cswish.model.PageElements;
import com.googlecode.cswish.struts.spring.ObjectFactory;
import com.googlecode.cswish.test.mock.struts2.MockValueStack;
import com.googlecode.cswish.test.mock.struts2.jsp.StrutsMockHttpServletRequest;
import com.googlecode.cswish.test.mock.struts2.jsp.StrutsMockHttpServletResponse;
import com.opensymphony.xwork2.inject.Container;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.util.ValueStack;

import freemarker.core.Environment;
import freemarker.ext.beans.StringModel;
import freemarker.template.Configuration;
import freemarker.template.SimpleHash;
import freemarker.template.SimpleNumber;
import freemarker.template.SimpleScalar;
import freemarker.template.SimpleSequence;
import freemarker.template.Template;
import freemarker.template.TemplateBooleanModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;

/**
 * Freemarker engine, @see StrutsFreemarkerManager
 * 
 * NOTICE: don't use @Service to avoid the circular reference
 * @author Jerry.Feng Date: 2008-6-6
 */
public class FreemarkerTplEngine {
	
	private static final Logger logger = Logger.getLogger(FreemarkerTplEngine.class);
	
	private static final FreemarkerTplEngine instance = new FreemarkerTplEngine();
	
	private final HashMap<String, Template> templates = new HashMap<String, Template>();

	private int nameIndex;
	
	private Functions functions;
	
	@Inject
	private FreemarkerManager freemarkerManager;

	@Inject
	private Container container;
	
	public FreemarkerTplEngine() {
		functions = ObjectFactory.getInstance().getBean(Functions.class, true);
	}
	
	public static FreemarkerTplEngine getInstance() {
		return instance;
	}
	
	private Template register(String content) {
		Template template = templates.get(content);
		if (template != null) {
			return template;
		}
		
		Reader reader = new StringReader(content);
		try {
			String name = "template" + (++nameIndex);
			template = new Template(name, reader, null);
			templates.put(content, template);
			return template;
		} catch (Exception ex) {
			logger.error("Wrong expression", ex);
			return null;
		}
	}
	
	public String execute(String content, Object model) {
		return execute(content, model, null);
	}
	
	/**
	 * Execute the simple freemarker template
	 * 
	 * @param content
	 * @param model
	 * @param resultTrace
	 * @return
	 */
	public String execute(String content, Object model, Map<String, Object> resultTrace) {
		Template template = templates.get(content);
		if (template == null) {
			template = register(content);
		}
		if (template != null) {
			try {
				StringWriter result = new StringWriter();
				Environment env = template.createProcessingEnvironment(model, result, null);
				env.setClassicCompatible(true);
				env.process();
				if (resultTrace != null) {
					for (Entry<String, Object> entry : resultTrace.entrySet()) {
						TemplateModel templateModel = env.getVariable(entry.getKey());
						if (templateModel instanceof SimpleScalar) {
							entry.setValue(((SimpleScalar)templateModel).getAsString());
						} else if (templateModel instanceof SimpleNumber) {
							entry.setValue(((SimpleNumber)templateModel).getAsNumber());
						} else if (templateModel instanceof TemplateBooleanModel){
							entry.setValue(((TemplateBooleanModel)templateModel).getAsBoolean());
						} else if (templateModel instanceof SimpleSequence){
							entry.setValue(((SimpleSequence)templateModel).toList());
						} else if (templateModel instanceof StringModel) {
							entry.setValue(((StringModel)templateModel).getWrappedObject());
						} else {
							// TODO: more type
							entry.setValue(templateModel);
						}
					}
				}
				return result.toString();
			} catch (Exception ex) {
				logger.debug("parse condition", ex);
			}
		}
		return "false";
	}
	
	public boolean verify(String content ,Object verifyModel) {
		Reader reader = new StringReader(content);
		try {
			String name = "template" + (++nameIndex);
			Template template = new Template(name, reader, null);
			SimpleHash model = new SimpleHash();
			model.put("model", verifyModel);
			model.put("fn", functions);
			FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
			return true;
		} catch (Exception ex) {
			logger.debug("Wrong expression", ex);
			return false;
		}
	}
	
	public String executePageTemplate(String content, String actionName, Object action, PageElements pageElements) throws IOException, TemplateException {
		return executePageTemplate(content, actionName, action, pageElements, false);
	}
	
	/**
	 * Prepare the action environment and execute the page template
	 * 
	 * @param content
	 * @param actionName
	 * @param action
	 * @param pageElements
	 * @param needFixFreemarkerPageContext
	 * 				FIXME: Parse two times - for the FreemarkerPageContext (necessary?)
	 * 				first time, the tag injects context to header tag
	 * 				second time, the header tag output the correct context
	 * 
	 * @return
	 * @throws IOException
	 * @throws TemplateException
	 */
	public String executePageTemplate(String content, String actionName, Object action, PageElements pageElements, boolean needFixFreemarkerPageContext) throws IOException, TemplateException {
		if (freemarkerManager == null || container == null) {
			ObjectFactory.getInstance().injectInternalBeans(this);
		}

		ServletContext servletContext = ServletActionContext.getServletContext();
		Configuration configuration = freemarkerManager.getConfiguration(servletContext);
		Reader reader = new StringReader(content);
		Template template = new Template(actionName, reader, configuration);

		StrutsMockHttpServletRequest request = new StrutsMockHttpServletRequest();
		request.setupGetContext(servletContext.getContextPath());
		HttpServletResponse response = new StrutsMockHttpServletResponse();
		ValueStack stack = new MockValueStack(container);

		Map<String, Object> invocationContext = stack.getContext();
		PageElements oldPageElements = (PageElements) invocationContext.get(FrameConstant.STACK_PAGEELEMENTS);
		CharArrayWriter parentCharArrayWriter = new CharArrayWriter();
		String oldActionName = null;
		try {
			invocationContext.put(FrameConstant.STACK_PAGEELEMENTS, pageElements);
			invocationContext.put("action", action);
			oldActionName = (String) invocationContext.put(FrameConstant.STACK_ACTIONNAME, actionName);
			ScopesHashModel model = freemarkerManager.buildTemplateModel(stack, action, servletContext, request, response, configuration.getObjectWrapper());
			
			if (needFixFreemarkerPageContext) {
				template.process(model, new CharArrayWriter());
				pageElements.safeContext().setProcessed(false);
			}
			template.process(model, parentCharArrayWriter);
		} finally {
			invocationContext.put(FrameConstant.STACK_PAGEELEMENTS, oldPageElements);
			invocationContext.put(FrameConstant.STACK_ACTIONNAME, oldActionName);
		}
		return parentCharArrayWriter.toString();
	}
}